/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

import $ from "jquery";
import cockpit from "cockpit";

import { mustache } from "mustache";

import { machines } from "machines";
import * as credentials from "credentials";
import "patterns";

import add_tmpl from "raw-loader!machine-add.html";
import auth_failed_tmpl from "raw-loader!machine-auth-failed.html";
import change_auth_tmpl from "raw-loader!machine-change-auth.html";
import change_port_tmpl from "raw-loader!machine-change-port.html";
import color_picker_tmpl from "raw-loader!machine-color-picker.html";
import invalid_hostkey_tmpl from "raw-loader!machine-invalid-hostkey.html";
import not_supported_tmpl from "raw-loader!machine-not-supported.html";
import sync_users_tmpl from "raw-loader!machine-sync-users.html";
import unknown_hosts_tmpl from "raw-loader!machine-unknown-hostkey.html";

const _ = cockpit.gettext;

var default_codes = {
    "no-cockpit": "not-supported",
    "not-supported": "not-supported",
    "protocol-error": "not-supported",
    "authentication-not-supported": "change-auth",
    "authentication-failed": "change-auth",
    "no-forwarding": "change-auth",
    "unknown-hostkey": "unknown-hostkey",
    "invalid-hostkey": "invalid-hostkey",
    "not-found": "add-machine",
    "unknown-host": "unknown-host",
    "sync-users": "sync-users"
};

function translate_and_init(tmpl) {
    var tmp = $("<div>").append(tmpl);
    tmp.find("[translate=\"yes\"]").each(function(i, e) {
        var old = e.outerHTML;
        var translated = cockpit.gettext(e.getAttribute("context"), $(e).text());
        $(e).removeAttr("translate")
                .text(translated);
        tmpl = tmpl.replace(old, e.outerHTML);
    });
    mustache.parse(tmpl);
    return tmpl;
}

var templates = {
    "add-machine" : translate_and_init(add_tmpl),
    "auth-failed" : translate_and_init(auth_failed_tmpl),
    "change-auth" : translate_and_init(change_auth_tmpl),
    "change-port" : translate_and_init(change_port_tmpl),
    "color-picker" : translate_and_init(color_picker_tmpl),
    "invalid-hostkey" : translate_and_init(invalid_hostkey_tmpl),
    "not-supported" : translate_and_init(not_supported_tmpl),
    "sync-users" : translate_and_init(sync_users_tmpl),
    "unknown-hostkey" : translate_and_init(unknown_hosts_tmpl),
    "unknown-host" : translate_and_init(unknown_hosts_tmpl)
};

function full_address(machines_ins, address) {
    var machine = machines_ins.lookup(address);
    if (machine && machine.address != "localhost")
        return machine.connection_string;

    return address;
}

function Dialog(selector, address, machines_ins, codes) {
    var self = this;

    self.machines_ins = machines_ins;
    self.codes = codes;
    self.address = full_address(self.machines_ins, address);

    var promise_callback = null;

    var success_callback = null;

    var current_template = null;
    var current_instance = null;

    function address_or_label() {
        var machine = self.machines_ins.lookup(self.address);
        var host = self.machines_ins.split_connection_string(self.address).address;
        if (machine && machine.label)
            host = machine.label;
        return host;
    }

    function change_content(template, error_options) {
        var old_instance = current_instance;

        if (current_template === template)
            return;

        if (template == "add-machine")
            current_instance = new AddMachine(self);
        else if (template == "sync-users")
            current_instance = new SyncUsers(self);
        else if (template == "unknown-hostkey" || template == "unknown-host")
            current_instance = new HostKey(self, template);
        else if (template == "invalid-hostkey")
            current_instance = new HostKey(self, template);
        else if (template == "change-auth")
            current_instance = new ChangeAuth(self);
        else if (template == "change-port")
            current_instance = new MachinePort(self);
        else
            current_instance = new Simple(self);

        current_template = template;
        current_instance.load(error_options);

        if (old_instance && old_instance.close)
            old_instance.close();
        old_instance = null;
    }

    self.try_to_connect = function(address, options) {
        var dfd = $.Deferred();
        var conn_options = $.extend({
            payload: "echo",
            host: address
        },
                                    options);

        var machine = self.machines_ins.lookup(address);
        if (machine && machine.host_key && !machine.on_disk) {
            conn_options['temp-session'] = false; /* Compatiblity option */
            conn_options.session = 'shared';
            conn_options['host-key'] = machine.host_key;
        }
        var client = cockpit.channel(conn_options);
        client.send("x");
        $(client)
                .on("message", function() {
                    $(client).off();
                    client.close();
                    dfd.resolve();
                })
                .on("close", function(event, options) {
                    dfd.reject(options);
                });

        return dfd.promise();
    };

    self.get_sel = function(child_selector) {
        var ret_txt = selector;
        if (child_selector)
            ret_txt = ret_txt + " " + child_selector;
        return $(ret_txt);
    };

    self.set_on_success = function (callback) {
        success_callback = callback;
    };

    self.set_goal = function (callback) {
        promise_callback = callback;
    };

    self.complete = function(val) {
        if (success_callback)
            success_callback(val);
        else
            $(selector).modal('hide');
    };

    self.render = function render(data, template) {
        if (!template)
            template = current_template;

        var address_data = self.machines_ins.split_connection_string(self.address);
        var context = $.extend({
            host : address_or_label(),
            full_address : self.address,
            context_title : self.context_title,
            strong : function() {
                return function(text, render) {
                    return "<strong>" + render(text) + "</strong>";
                };
            }
        }, data, address_data);

        var output = $(mustache.render(templates[template], context));
        cockpit.translate(output);
        self.get_sel(".modal-content").html(output);
    };

    self.render_error = function render_error(error) {
        var template;
        if (error.problem && error.command == "close")
            template = self.codes[error.problem];

        if (template && current_template !== template)
            change_content(template, error);
        else
            $(selector).dialog("failure", cockpit.message(error));
    };

    self.clear_error = function clear_error() {
        $(selector).dialog("clear_errors");
    };

    self.render_template = function render_template(template) {
        change_content(template);
    };

    self.show = function () {
        var sel = self.get_sel();
        sel.on('hide.bs.modal', function () {
            self.get_sel(".model-content").empty();
        });
        sel.modal('show');
    };

    self.run = function (promise, failure_callback) {
        var dialog_dfd = $.Deferred();
        var promise_funcs = [];

        function next(i) {
            promise_funcs[i]()
                    .done(function(val) {
                        i = i + 1;
                        if (i < promise_funcs.length) {
                            next(i);
                        } else {
                            dialog_dfd.resolve();
                            self.complete(val);
                        }
                    })
                    .fail(function(ex) {
                        if (failure_callback)
                            failure_callback(ex);
                        else
                            self.render_error(ex);
                        dialog_dfd.reject(ex);
                    });
        }

        promise_funcs.push(function() {
            return promise;
        });

        self.get_sel().dialog("wait", dialog_dfd.promise());
        if (promise_callback)
            promise_funcs.push(promise_callback);

        next(0);
    };
}

function is_method_supported(methods, method) {
    var result = methods[method];
    return result ? result != "no-server-support" : false;
}

function MachineColorPicker(machines_ins) {
    var self = this;

    self.render = function(selector, address, selected_color) {
        var machine;

        if (address && !selected_color) {
            machine = machines_ins.lookup(address);
            if (machine)
                selected_color = machine.color;
        }

        if (!selected_color)
            selected_color = machines_ins.unused_color();

        var part;
        var colors = [];
        for (var i = 0; i < machines.colors.length; i += 6) {
            part = machines.colors.slice(i, i + 6);
            colors.push({ list : part });
        }

        var text = mustache.render(templates["color-picker"], { colors : colors, });
        $(selector).html(text);

        $("#host-edit-color", selector).css("background-color", selected_color);
        $(".color-cell", selector).each(function(index) {
            $(this).css("background-color", machines.colors[index]);
        });

        $('#host-edit-color-popover .popover-content .color-cell', selector)
                .click(function() {
                    var color = $(this).css('background-color');
                    $('#host-edit-color', selector).css('background-color', color);
                });

        $("#host-edit-color", selector).parent()
                .on('show.bs.dropdown', function () {
                    var $div = $('#host-edit-color', selector);
                    var $pop = $('#host-edit-color-popover', selector);
                    var div_pos = $div.position();
                    var div_width = $div.width();
                    var div_height = $div.height();
                    var pop_width = $pop.width();
                    var pop_height = $pop.height();

                    var top = div_pos.top - pop_height + 10;
                    if (top < 0) {
                        top = div_pos.top + div_height + 10;
                        $pop.addClass("bottom");
                        $pop.removeClass("top");
                    } else {
                        $pop.addClass("top");
                        $pop.removeClass("bottom");
                    }
                    $pop.css('left', div_pos.left + (div_width - pop_width) / 2);
                    $pop.css('top', top);
                    $pop.show();
                })
                .on('hide.bs.dropdown', function () {
                    $('#host-edit-color-popover', selector).hide();
                });
    };
}

function Simple(dialog) {
    var self = this;

    self.load = function() {
        dialog.render();
    };
}

function AddMachine(dialog) {
    var self = this;
    var color = null;
    var selector = dialog.get_sel();
    var run_error = null;

    var invisible = dialog.machines_ins.addresses.filter(function(addr) {
        var m = dialog.machines_ins.lookup(addr);
        return !m || !m.visible;
    });

    function existing_error(address) {
        var ex = null;
        var machine = dialog.machines_ins.lookup(address);
        if (machine && machine.visible && machine.on_disk) {
            ex = new Error(_("This machine has already been added."));
            ex.target = "#add-machine-address";
        }
        return ex;
    }

    function check_address(evt) {
        var disabled = true;
        var ex = null;

        var addr = $('#add-machine-address').val();
        var button = dialog.get_sel(".modal-footer>.btn-primary");
        if (addr === "") {
            disabled = true;
        } else if (!machines.allow_connection_string &&
                   (addr.indexOf('@') > -1 || addr.indexOf(':') > -1)) {
            ex = new Error(_("This version of cockpit-ws does not support connecting to a host with an alternate user or port"));
        } else if (addr.search(/\s+/) === -1) {
            ex = existing_error(addr);
            if (!ex)
                disabled = false;
        } else {
            ex = new Error(_("The IP address or host name cannot contain whitespace."));
        }

        if (ex)
            ex.target = "#add-machine-address";

        if (run_error)
            selector.dialog("failure", run_error, ex);
        else
            selector.dialog("failure", ex);

        button.prop("disabled", disabled);
    }

    function add_machine() {
        run_error = null;
        dialog.address = $('#add-machine-address').val();
        color = machines.colors.parse($('#add-machine-color-picker #host-edit-color').css('background-color'));
        if (existing_error(dialog.address))
            return;

        dialog.set_goal(function() {
            var dfp = $.Deferred();
            dialog.machines_ins.add(dialog.address, color)
                    .done(dfp.resolve)
                    .fail(function (ex) {
                        var msg = cockpit.format(_("Failed to add machine: $0"),
                                                 cockpit.message(ex));
                        dfp.reject(msg);
                    });

            return dfp.promise();
        });

        dialog.run(dialog.try_to_connect(dialog.address), function (ex) {
            if (ex.problem == "no-host") {
                var host_id_port = dialog.address;
                var port_index = host_id_port.lastIndexOf(":");
                var port = "22";
                if (port_index === -1)
                    host_id_port = dialog.address + ":22";
                else
                    port = host_id_port.substr(port_index + 1);
                ex.message = cockpit.format(_("Cockpit could not contact the given host $0. Make sure it has ssh running on port $1, or specify another port in the address."), host_id_port, port);
                ex = cockpit.message(ex);
                run_error = ex;
            }
            dialog.render_error(ex);
        });
    }

    self.load = function() {
        var manifest = cockpit.manifests.shell || {};
        var limit = parseInt(manifest["machine-limit"], 10);
        var color_picker = new MachineColorPicker(dialog.machines_ins);
        if (!limit || isNaN(limit))
            limit = 20;

        dialog.render({
            nearlimit : limit * 0.75 <= dialog.machines_ins.list.length,
            limit : limit,
            placeholder : _("Enter IP address or host name"),
            options : invisible,
        });

        var button = dialog.get_sel(".modal-footer>.btn-primary");
        button.on("click", add_machine);

        $("#add-machine-address").on("keyup", function (ev) {
            if (ev.which === 13) {
                var disabled = button.prop('disabled');
                if (!disabled)
                    add_machine();
            }
        });
        $("#add-machine-address").on("input focus", check_address);
        color_picker.render("#add-machine-color-picker", dialog.address, color);
        $("#add-machine-address").focus();
    };
}

function MachinePort(dialog) {
    var self = this;

    function change_port() {
        var dfp = $.Deferred();
        var parts = dialog.machines_ins.split_connection_string(dialog.address);
        parts.port = $("#edit-machine-port").val();
        var address = dialog.machines_ins.generate_connection_string(parts.user,
                                                                     parts.port,
                                                                     parts.address);
        function update_host(ex) {
            dialog.address = address;
            dialog.machines_ins.change(parts.address, { port: parts.port })
                    .done(function () {
                    // We failed before so try to connect again
                    // now that the machine is saved.
                        if (ex) {
                            dialog.try_to_connect(address)
                                    .done(dialog.complete)
                                    .fail(function (e) {
                                        dfp.reject(e);
                                    });
                        } else {
                            dfp.resolve();
                        }
                    })
                    .fail(function (ex) {
                        var msg = cockpit.format(_("Failed to edit machine: $0"),
                                                 cockpit.message(ex));
                        dfp.reject(msg);
                    });
        }

        dialog.try_to_connect(address)
                .done(function () {
                    update_host();
                })
                .fail(function (ex) {
                /* any other error means progress, so save */
                    if (ex.problem != 'no-host')
                        update_host(ex);
                    else
                        dfp.reject(ex);
                });

        dialog.run(dfp.promise());
    }

    self.load = function() {
        var machine = dialog.machines_ins.lookup(dialog.address);
        if (!machine) {
            dialog.get_sel().modal('hide');
            return;
        }

        dialog.render({
            port : machine.port,
            allow_connection_string : machines.allow_connection_string
        });
        if (machines.allow_connection_string)
            dialog.get_sel(".modal-footer>.btn-primary").on("click", change_port);
    };
}

function HostKey(dialog, problem) {
    var self = this;
    var error_options = null;
    var key = null;
    var allow_change = (problem == "unknown-hostkey" ||
                        problem == "unknown-host");

    function add_key() {
        var q;
        var machine = dialog.machines_ins.lookup(dialog.address);
        if (!machine || machine.on_disk) {
            q = dialog.machines_ins.add_key(key);
        } else {
            /* When machine isn't saved to disk
               don't save the key either */
            q = dialog.machines_ins.change(dialog.address, {
                host_key: key
            });
        }

        var promise = q.then(function () {
            var inner = dialog.try_to_connect(dialog.address);

            inner.fail(function(ex) {
                if ((ex.problem == "invalid-hostkey" ||
                    ex.problem == "unknown-hostkey") &&
                    machine && !machine.on_disk) {
                    dialog.machines_ins.change(dialog.address, {
                        host_key: null
                    });
                }
            });

            return inner;
        });

        dialog.run(promise);
    }

    function render() {
        var promise = null;
        var options = {};
        var match_problem = problem;
        var fp;

        if (error_options) {
            key = error_options["host-key"];
            fp = error_options["host-fingerprint"];
        }

        dialog.render({
            context_title : dialog.context_title,
            key : fp,
        });

        if (!key) {
            if (problem == "unknown-host") {
                options.session = "private";
                match_problem = "unknown-hostkey";
            }

            promise = dialog.try_to_connect(dialog.address, options)
                    .fail(function(ex) {
                        if (ex.problem != match_problem) {
                            dialog.render_error(ex);
                        } else {
                            error_options = ex;
                            render();
                        }
                    })

            // Fixed already, just close
                    .done(function (v) {
                        dialog.complete(v);
                    });

            dialog.get_sel().dialog("wait", promise);
        } else if (allow_change) {
            dialog.get_sel(".modal-footer>.btn-primary").on("click", add_key);
        }
    }

    self.load = function(ex) {
        error_options = ex;
        render();
    };
}

function ChangeAuth(dialog) {
    var self = this;
    var error_options = null;
    var keys = null;
    var have_ticket = null;
    var machine = dialog.machines_ins.lookup(dialog.address);

    self.user = { };

    function update_available() {
        var key_div = dialog.get_sel('.keys');
        var key_locked_div = dialog.get_sel('.key-locked');
        var have_keys = false;

        if (key_div) {
            key_div.empty();
            for (var id in keys.items) {
                var key = keys.items[id];
                if (key.loaded) {
                    have_keys = true;
                    key_div.append($("<div>").text(key.name || key.comment));
                }
            }
        }

        if (key_locked_div && error_options &&
            error_options.error && error_options.error.startsWith("locked identity")) {
            const identity_path = error_options.error.split(": ")[1];
            key_locked_div.find(".locked-identity").text(identity_path);
            key_locked_div.find(".btn-primary").on("click", () => {
                key_locked_div.find(".btn-primary").attr("disabled", true);
                keys.load(identity_path, key_locked_div.find(".locked-identity-password").val())
                        .then(() => dialog.clear_error())
                        .catch(ex => {
                            dialog.render_error(ex);
                            key_locked_div.find(".btn-primary").attr("disabled", false);
                        });
            });

            for (const id in keys.items) {
                const key = keys.items[id];
                if (key.name === identity_path.split("/").pop() && key.loaded)
                    dialog.get_sel(".login-locked").empty();
            }
        }

        var empty_div = dialog.get_sel('.empty');
        if (empty_div)
            empty_div.toggleClass("hidden", have_keys);
    }

    function login() {
        var address;
        var options = {};
        var dfp = $.Deferred();
        var user = $("#login-custom-user").val();

        var parts = dialog.machines_ins.split_connection_string(dialog.address);
        parts.user = user;

        address = dialog.machines_ins.generate_connection_string(parts.user,
                                                                 parts.port,
                                                                 parts.address);

        if ($("#login-type button").val() != 'stored') {
            options.password = $("#login-custom-password").val();
            options.session = 'shared';
            if (!user) {
                /* we don't want to save the default user for everyone
                 * so we pass current user as an option, but make sure the
                 * session isn't private
                 */
                if (self.user && self.user.name)
                    options.user = self.user.name;
                options["temp-session"] = false; /* Compatibility option */
            }
        }

        dialog.try_to_connect(address, options)
                .done(function () {
                    dialog.address = address;
                    if (machine) {
                        dialog.machines_ins.change(machine.address, { user : user })
                                .done(dfp.resolve)
                                .fail(dfp.reject);
                    } else {
                        dfp.resolve();
                    }
                })
                .fail(dfp.reject);

        dialog.run(dfp.promise());
    }

    function change_login_type(value) {
        var stored = value != 'password';
        var text = $("#login-type li[value=" + value + "]").text();
        $("#login-type button").val(value);
        $("#login-type button span").text(text);
        $("#login-available").toggle(stored);
        $("#login-diff-password").toggle(!stored);
    }

    function render() {
        var promise = null;
        var template = "change-auth";
        if (!machines.allow_connection_string || !machines.has_auth_results)
            template = "auth-failed";

        var no_password = false;
        var methods = null;
        var available = null;
        var locked_identity = false;

        var machine_user = dialog.machines_ins.split_connection_string(dialog.address).user;
        if (!machine_user && machine)
            machine_user = machine.user;

        if (error_options && machines.has_auth_results) {
            available = {};

            methods = error_options["auth-method-results"];
            if (methods) {
                no_password = methods.password === "not-provided";
                for (var method in methods) {
                    if (is_method_supported(methods, method)) {
                        available[method] = true;
                    }
                }
            }

            if ($.isEmptyObject(available))
                template = "auth-failed";

            locked_identity = error_options.error && error_options.error.startsWith("locked identity");
        }

        dialog.render({
            supported : methods,
            available : available,
            machine_user : machine_user,
            default_user : self.user ? self.user.name : "",
            show_password : available && available.password && !no_password,
            show_ticket: available && available['gssapi-mic'] && have_ticket,
            show_locked_identity: available && available['public-key'] && locked_identity,
            can_sync: !!dialog.codes['sync-users'],
            'machines.allow_connection_string' : machines.allow_connection_string,
            sync_link : function() {
                return function(text, render) {
                    return '<a tabindex="0" id="do-sync-users">' + render(text) + "</a>";
                };
            }
        }, template);

        if (methods === null && machines.has_auth_results) {
            promise = dialog.try_to_connect(dialog.address)
                    .fail(function(ex) {
                        error_options = ex;
                        render();
                    })

            // Fixed already, just close
                    .done(function (v) {
                        dialog.complete(v);
                    });

            dialog.get_sel().dialog("wait", promise);
        } else if (!$.isEmptyObject(available)) {
            $("#login-type li").on('click', function() {
                change_login_type($(this).attr("value"));
            });
            change_login_type($("#login-type li:first-child").attr("value"));
            dialog.get_sel(".modal-footer>.btn-primary").on("click", login);
            dialog.get_sel("a[data-content]").popover();

            update_available();
        }

        dialog.get_sel("#do-sync-users").on("click", function () {
            dialog.render_template("sync-users");
        });
    }

    self.load = function(ex) {
        error_options = ex;
        if (credentials) {
            keys = credentials.keys_instance();
            $(keys).on("changed", update_available);
        }

        cockpit.spawn(["klist", "-s"])
                .fail(function (ex) {
                    have_ticket = false;
                })
                .done(function (done) {
                    have_ticket = true;
                })
                .always(function () {
                    cockpit.user()
                            .done(function (user) {
                                self.user = user;
                            })
                            .always(function (user) {
                                render();
                            });
                });
    };

    self.close = function(ex) {
        if (keys) {
            $(keys).off();
            keys.close();
        }
        keys = null;
    };
}

function SyncUsers(dialog) {
    var self = this;
    var users = {};

    var needs_auth = false;
    var needs_root = false;
    var methods = null;
    var remote_options = { superuser: true };

    var perm_failed = null;

    function load_users() {
        var local = cockpit.dbus(null, {
            bus: "internal",
            host: "localhost",
            superuser: true
        });
        $(local).on("close", function(event, options) {
            perm_failed = options;
            render();
        });

        var proxy = local.proxy("cockpit.Setup", "/setup");
        proxy.wait(function () {
            if (proxy.valid) {
                var blank = {
                    t : "(asas)",
                    v : [[], []]
                };

                proxy.Transfer("passwd1", blank)
                        .done(function(prepared) {
                            var i, parts, name;
                            var groups = prepared.v[1];

                            for (i = 0; i < prepared.v[0].length; i++) {
                                var raw = prepared.v[0][i];

                                parts = raw.split(":");
                                name = parts[0];

                                users[name] = {
                                    username : name,
                                    name : parts[4] || name,
                                    raw : raw,
                                    groups : [],
                                };
                            }

                            for (i = 0; i < groups.length; i++) {
                                parts = groups[i].split(":");
                                name = parts[0];
                                var members = parts[parts.length - 1].split(",");
                                for (var j = 0; j < members.length; j++) {
                                    var u = members[j];
                                    if (users[u])
                                        users[u].groups.push(name);
                                }
                            }
                        })
                        .fail(function(ex) {
                            ex.message = cockpit.gettext(ex.message);
                            perm_failed = ex;
                        })
                        .always(function(ex) {
                            $(local).off();
                            local.close();
                            render();
                        });
            }
        });
    }

    function sync_users() {
        var client = null;

        var dfd = $.Deferred();
        var promise = dfd.promise();

        dialog.run(promise);

        /* A successfull sync should close the dialog */
        dialog.set_on_success(null);

        promise.always(function () {
            if (client) {
                $(client).off();
                client.close();
            }
        });

        var options = { bus: "internal" };
        if (needs_auth) {
            options.user = $("#sync-username").val();
            options.password = $("#sync-password").val();
        }
        $.extend(options, remote_options, { host: dialog.address });
        client = cockpit.dbus(null, options);
        $(client).on("close", function(event, ex) {
            dfd.reject(cockpit.message(ex));
        });

        var variant = {
            t : "(asas)",
            v : [[]],
        };

        var groups = {};
        dialog.get_sel("input:checked").each(function() {
            var u = users[$(this).attr("name")];
            if (u) {
                variant.v[0].push(u.raw);
                for (var i = 0; i < u.groups.length; i++) {
                    var group = u.groups[i];
                    if (!groups[group])
                        groups[group] = [];

                    groups[group].push(u.username);
                }
            }
        });
        variant.v.push(Object.keys(groups).map(function(k) {
            return k + ":::" + groups[k].join();
        }));

        /* We assume all cockpits support the 'passwd1' mechanism */
        var proxy = client.proxy("cockpit.Setup", "/setup");
        proxy.wait(function() {
            if (proxy.valid) {
                proxy.Commit("passwd1", variant)
                        .fail(function(ex) {
                            ex.message = cockpit.gettext(ex.message);
                            dfd.reject(ex);
                        })
                        .done(dfd.resolve);
            }
        });
    }

    function toggle_button() {
        var any = dialog.get_sel("input:checked").length > 0;
        dialog.get_sel(".modal-footer>.btn-primary").toggleClass("disabled", !any);
    }

    function render() {
        function formated_groups() {
            if (this.groups)
                return this.groups.join(", ");
        }

        /* assume password is allowed for backwards compatibility */
        var allows_password = true;
        var user_list = Object.keys(users).sort()
                .map(function(v) {
                    return users[v];
                });

        if (machines.has_auth_results && methods)
            allows_password = is_method_supported(methods, 'password');

        var text = dialog.render({
            needs_auth : needs_auth,
            needs_root : needs_root,
            users : user_list,
            perm_failed : perm_failed ? cockpit.message(perm_failed) : null,
            allows_password : allows_password,
            formated_groups: formated_groups,
        });

        dialog.get_sel(".modal-content").html(text);
        dialog.get_sel(".modal-footer>.btn-primary").on("click", sync_users);
        dialog.get_sel("input:checkbox").on("change", function() {
            var name = $(this).attr("name");
            users[name].checked = $(this).is(':checked');
            toggle_button();
        });
        toggle_button();
    }

    self.load = function(error_options) {
        if (error_options)
            methods = error_options['auth-method-results'];

        render();
        dialog.try_to_connect(dialog.address, remote_options).fail(function(ex) {
            needs_auth = true;
            if (ex.problem == "access-denied") {
                needs_root = true;
                if (!methods && machines.has_auth_results)
                    /* TODO: We need to know if password auth is
                     * supported but we only get that when the transport
                     * closes. Passing an invalid username should
                     * open new transport that fails.
                     */
                    dialog.try_to_connect(dialog.address, { user : "1" })
                            .fail(function(ex) {
                                methods = ex['auth-method-results'];
                            })
                            .always(render);
            } else {
                methods = ex['auth-method-results'];
                render();
            }
        });
        load_users();
    };
}

function MachineDialogManager(machines_ins, codes) {
    var self = this;

    if (!codes)
        codes = default_codes;

    var color_picker = new MachineColorPicker(machines_ins);

    self.troubleshoot = function(target_id, machine) {
        var selector = "#" + target_id;
        if (!machine || !machine.problem)
            return;

        var template = codes[machine.problem];
        if (machine.problem == "no-host")
            template = "change-port";

        var dialog = new Dialog(selector, machine.address, machines_ins, codes);
        dialog.render_template(template);
        dialog.show();
    };

    self.needs_troubleshoot = function (machine) {
        if (!machine || !machine.problem)
            return false;

        if (machine.problem == "no-host")
            return true;

        return !!codes[machine.problem];
    };

    self.render_dialog = function (template, target_id, address) {
        var selector = "#" + target_id;
        var dialog = new Dialog(selector, address, machines_ins, codes);
        dialog.render_template(template);
        dialog.show();
    };

    self.render_color_picker = function (selector, address) {
        color_picker.render(selector, address);
    };
}

export function new_machine_dialog_manager(machines_ins, codes) {
    return new MachineDialogManager(machines_ins, codes);
}
